JAVA并发编程

简介:JAVA 线程,JMM,DCL,CAS

基础知识

线程

线程的5种状态

  • New:新建
  • Runnable:就绪
  • Running:运行中
  • Blocked:阻塞
  • Dead:死亡

wait/notify/notifyAll

  • Object 类中方法,为native ,也就是说每个对象都有
  • wait(1000) ,如果没有notify或notifAll方法的唤醒,也会自动唤醒
  • 这3个方法必须要在同步块中,因为线程等待需要获取锁,如果没有同步锁,如何获取monitor

sleep/yield/join

  • 在Thread 类中
  • sleep(1000),线程休眠1000,不依赖于同步块,让出CPU,但不释放锁
  • yield 暂停线程,改变线程执行状态从 Runing->Runable
  • join 父线程等待子线程执行完成后再执行,就是将异步执行的线程合并为同步的线程,等待加入的线程执行完之后再执行自己

Java内存模型(JMM)

重排序与顺序一致性

Java的并发模型采用的是共享内存模型,java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

JMM对共享内存的操作做出了如下两条规定:

  • 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  • 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。(编译器重排序)
  2. 指令级并行的重排序。现代处理采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应及其指令的执行顺序。(处理器重排序)
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。(处理器重排序)

并不是所有的都会进行重排序

  • happens-before
  • as-if-serial

Double Check Lock(DCL)

JAVA多线程编程中的双重检查锁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingleModel4 {

// 静态常量,随class初始化
public static volatile SingleModel4 model;
// 私有构造方法,其他地方不可调用
private SingleModel4() {
};
// 懒汉,等需要的时候再创建
public static SingleModel4 getSingle() {
if (model == null){ //1
synchronized (SingleModel4.class) { //2
if(model==null){ //3
model = new SingleModel4(); //4
}
}
}
return model;
}
}
DCL (double check lock) java 多线程双重检查锁定
原因在于当线程A或者到锁进行到4时,由于4的重排序,先对model地址进行赋值,然后再初始,那么此时,如果有线程B进入到1,会判断model不为空,但其实model 还未初始化
解决方式:加上volatile SingleModel4 model;

compare-and-swap(CAS)

一种无锁的原子性操作,性能出色,但会产生ABA问题,后面那个A是其他线程改的,解决方式是加上版本号,1A-2B-3A

AbstractQueuedSynchronizer

背景

  • 在jdk1.5的时候,synchronized 是重量级的,在线程切换的时候耗时严重,同时中断等操作不方便,再加上没有相关的工具类。
  • Doug Lea 设计一个通用的同步器,可方便用户扩展自己的同步器

    设计思路

    同步器的核心方法是acquire和release操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
    }
    public final boolean release(int arg) {
    if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
    return true;
    }
    return false;
    }

为了实现这两个操作,需要协调三大关键操作引申出来的三个基本组件:

  • 同步器状态的原子性管理
  • 线程阻塞与解除阻塞
  • 队列的管理
    同步状态

    AQS类使用单个int(32位)来保存同步状态,并暴露出getState、setState以及compareAndSet操作来读取和更新这个同步状态。其中属性state被声明为volatile,并且通过使用CAS指令来实现compareAndSetState,使得当且仅当同步状态拥有一个一致的期望值的时候,才会被原子地设置成新值,这样就达到了同步状态的原子性管理,确保了同步状态的原子性、可见性和有序性。

阻塞

 j.u.c.locks包提供了LockSupport类来解决这个问题。方法LockSupport.park阻塞当前线程直到有个LockSupport.unpark方法被调用。unpark的调用是没有被计数的,因此在一个park调用前多次调用unpark方法只会解除一个park操作。另外,它们作用于每个线程而不是每个同步器。一个线程在一个新的同步器上调用park操作可能会立即返回,因为在此之前可以有多余的unpark操作。但是,在缺少一个unpark操作时,下一次调用park就会阻塞。虽然可以显式地取消多余的unpark调用,但并不值得这样做。在需要的时候多次调用park会更高效。park方法同样支持可选的相对或绝对的超时设置,以及与JVM的Thread.interrupt结合 ,可通过中断来unpark一个线程。

队列

同步队列:整个框架的核心就是如何管理线程阻塞队列,该队列是严格的FIFO队列,因此不支持线程优先级的同步。同步队列的最佳选择是自身没有使用底层锁来构造的非阻塞数据结构,业界主要有两种选择,一种是MCS锁,另一种是CLH锁。其中CLH一般用于自旋,但是相比MCS,CLH更容易实现取消和超时,所以同步队列选择了CLH作为实现的基础

条件队列:AQS只有一个同步队列,但是可以有多个条件队列。AQS框架提供了一个ConditionObject类,给维护独占同步的类以及实现Lock接口的类使用。 ConditionObject类实现了Condition接口,Condition接口提供了类似Object管程式的方法,如await、signal和signalAll操作,还扩展了带有超时、检测和监控的方法。ConditionObject类有效地将条件与其它同步操作结合到了一起。该类只支持Java风格的管程访问规则,这些规则中,当且仅当当前线程持有锁且要操作的条件(condition)属于该锁时,条件操作才是合法的。这样,一个ConditionObject关联到一个ReentrantLock上就表现的跟内置的管程(通过Object.wait等)一样了。两者的不同仅仅在于方法的名称、额外的功能以及用户可以为每个锁声明多个条件

线程安全

由于存在竞争条件,线程不安全,必须要保证原子性操作,才能消除这种不安全性。
synchronized 关键字,悲观锁,互斥锁,其他未获取到锁的线程必须等待(阻塞)
锁由于独占性,加锁会影响性能,必须保证一个平衡

synchronized

  • synchronized 通过对象内部的一个叫监视器(monitor)来实现,而监视器依赖底层Mutex指令实现
  • 操作系统实现线程切换,需要从用户态转换到核心态,其成本会很高,这也就是synchronized 效率低下的原因,故synchronized被称为重量级锁
  • jdk 1.6之后,为减少性能损耗,对synchronized进行优化,引入了 轻量级锁 与 偏向锁

如果锁的释放很容易,那么获取锁的线程如果进入自循环的话,会很容易等到其他线程释放锁。这种叫自旋锁,不会切换状态,但会空转CPU,所以需要权衡

synchronized 锁升级状态

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

  • 无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
  • 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
  • 轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
  • 重量级锁:若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
1
2
3
4
5
-XX:-UseBiasedLocking 关闭偏向锁
JDK 1.6 默认开启偏向锁:尝试把锁给访问它的第一个线程
JDK1.6中-XX:+UseSpinning开启自旋锁;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;

volatile 写读的内存语义

  • 当写一个volatile 变量时,JMM会把该现场对应的本地内存中的共享变量刷到主内存
  • 当读一个volatile 变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量

使用场景

  • volatile 可见性,被修饰表示不会与其他的内存操作一起被重排序

    使用场景,状态表计量(就是前面的线程运行状态那种,只用了它的可见性);double check (单例声明)

组合对象

  • 将需要线程安全的交给已经安全的工具类
  • 同步策略的文档化

构建块

  • 同步容器:包含2部分

    • 一个是Vector 和 Hashtable (使用的 synchronized )
    • 另一个是同步包由(Collections.synchronizedXxx()工厂方法创建)

      同步容器是线程安全的,但是有些复合操作,如反复迭代,缺少及加入(put-if-absent),在并发的时候会出问题

  • 并发容器:Java 1.5 提供几种并发的容器来改善同步容器,通过对容器的所有状态进行串行访问

    • ConcurrentHashMap –> 替代同步的哈希Map实现
    • CopyOnWriteArrayList –>List 相应的同步实现
  • Java 1.5 还新增了2个新的容器类型,Queue 和 BlockingQueue

  • 闭锁

    • CountDownLatch :主要提供的机制是多个线程都达到了预期状态或完成预期工作时触发事件,其他线程可以等待这个事件来触发自己后续的工作,这里等待线程是可以多个
    • FutureTask :多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
    • Semaphore :Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的功能
    • CyclicBarrier :可以协同多个线程,让多个线程在这个屏障前等待,直到所有线程都达到屏障,再一起执行后面的动作

      CountDownLatch是不能循环使用,CyclicBarrier可以循环使用。就是CyclicBarrier可以多次被初始化。

任务执行

  • Executor 接口
1
2
3
public interface Executor{
void execute(Runable command)
}

为任务提交,与任务执行 解耦合

* newFixedThreadPool 定长线程池,
* newCachedThreadPool 可缓存线程池,长度不定,多任务时增加,少任务时减少
* newSingleThreadExecutor 单线程化
* newScheduleThreadPool 定长线程池,支持周期任务,类似Timer(取代Timer的缺点:TImer只会单线程执行,前一个任务如果耗时严重,会影响后一个任务的时序)
  • 执行策略

    • 任务在什么线程中执行
    • 任务以什么顺序执行
    • 可以有多少个任务并发执行
    • 可以有多少个任务等待执行
    • 如果系统过载,需要放弃,放弃那个任务,如何通知程序
    • 在任务执行前与结束后,应该做什么处理
  • Runnable vs Callable

    • 定义任务单元,Runnalbe 接口无返回,Callable可通过泛型指定返回类型
    • Runnable 在Thread类构造中,Callable 没有
  • Future

    • 对线程执行结果的一种返回包装,能够判断任务是否完成,是否中断,获取任务执行结果

取消和关闭任务

一般任务的取消可以通过 valatile boolean flag;这种取消状态来判定

  • 任务中断

    Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理

1
2
3
t.isInterrupted() 	    //对象方法:检查是否被设置为 中断 状态,不会改变状态
t.interrupt() //对象方法:设置线程为中断
Thread.interrupted() //类方法:清除 当前线程 中断 状态

但是,那些会抛出InterruptedException的方法要除外。像sleep、wait、notify、join,这些方法遇到中断必须有对应的措施,可以直接在catch块中处理,也可以抛给上一层。这些方法之所以会抛出InterruptedException就是由于Java虚拟机在实现这些方法的时候,本身就有某种机制在判断中断标识位,如果中断了,就抛出一个InterruptedException

  • Futrue .cancel() 可以取消任务
  • ExecutorService 提供shutdown 与 shutdownNow 来关闭线程池
  • JVM关闭时,可添加钩子函数,Runtime.addShutdownHook 来注册

线程池

  • 设置线程池大小
    • 对于计算密集型的任务,一个由N个处理器的系统可以通过设置一个 N+1个线程池来获取最优利用率
    • 对于IO密集型,可参看Runtime,getRuntime().availableProcessors() //获取CPU数量

活跃度危险

  • 死锁

    • 数据库中,如果两个事物发生死锁,它会选择一个牺牲者,使其退出事务,保证另一个事务的正常进行
    • JVM中,当两个线程发生死锁是,这些线程永远不能再使用。游戏结束
  • 锁顺序死锁

    两个线程试图通过不同的顺序获取多个相同的锁。

  • 识别与诊断死锁

    • 1锁当然不会死锁
    • 使用Lock类定义的tryLock 来代替使用内部锁机制
    • JVM挺实用线程转储(thread dump)识别死锁

显示锁

  • Lock vs ReentrantLock vs synchronized

    与内部锁不同在于,Lock 提供了无条件的,可轮询的,定时的,可中断的锁。
    与内部锁有着同样的内存语义,一样的可重入加锁

1
2
3
4
5
6
7
8
9
Lock lock = new ReentrantLock();
...
lock.lock();
try{
//更新对象状态
//捕获异常,必要时恢复到原路的不变约束
}finally{
lock.unlock();
}

显示的Lock 与内部锁相比提供了一些扩展性,包括处理不可用锁时更好的灵活性(可轮询和可自定时锁请求,可中断锁请求,非块结果锁)

  • 读写锁
1
2
3
4
public interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}

读写锁中的写入锁有一个唯一的使用者,也只能由获取到该锁的线程释放